查看原文
其他

APP去广告技术分析

I鲸落I@看雪 大白聊IT
2024-08-23

本文为看雪论坛优秀文章

看雪论坛作者ID:I鲸落I





准备工作


测试环境


1.测试机 Pixel 2(Android 10)

2.frida v15.1.27(objection 插件 v1.11.0)

3.开发者助手 v1.2.1

4.MT管理器 v13.3

5.jadx-gui v1.4.4

6.fiddler v5.0.20211.51073





去除广告思路


2.1 Android 广告


2.1.1 Android 中的广告形式


广告的表现形式很多,可能是一个界面(activity),可能是局部在上方或下方的一个区域视图(view)等。以下是常见广告形式:


1.嵌入式广告:将广告直接嵌入到应用程序中,通常出现在应用程序的底部、顶部或侧边栏。


2.插页式广告:应用程序的某个时间点弹出的广告,通常会覆盖整个屏幕。插页式广告通常在应用程序的特定事件之后出现,例如游戏中的关卡结束或应用程序的主菜单页面。


3.横幅广告:在应用程序的顶部或底部显示的广告,通常以图像或文本的形式出现。横幅广告通常比嵌入式广告小,不会占用应用程序的太多空间。


4.视频广告:在应用程序中播放的广告,通常以全屏或插页式的形式出现。通常需要观看一段时间才能跳过或关闭。


2.1.2 Android 广告来源


1.Push 推送广告:通过推送消息到用户设备通知栏上展示广告。


2.第三方 SDK 广告:很多应用都会集成第三方广告平台,比如 AdMob、Facebook Audience Network、Unity Ads 等等,应用程序可以用第三方广告 SDK 来从其他公司的广告库中获取广告并在应用程序中展示。


2.2 Android 去除广告思路


无论怎样形式、怎样来源的广告,在本地一定需要展示出来,展示就需要广告内容载体,如界面、视图等,对于这些容器,即可以利用静态的布局,也可以动态生成布局。如果能移除这些容器、或者破坏容器生成条件就可以达到去广告的地步。

◆对于静态布局的广告:广告图片视频都是保存在apk里的,只需要直接从配置清单 xml 文件,或相应的布局xml文件入手,修改容器的布局或者删除相应的代码,就可去除广告。
◆对于动态导入第三方SDK的广告:我们就需要从代码逻辑上入手。找到它动态导入广告的地方,尝试修改判断条件,从而使导入广告失败,或者让广告无法显示,从而去除广告。



 

本次案例是来自于第三方 SDK 软件的广告投放,通过发送请求包,从而获取相对应的广告 ID 与资源,对于这种情况,我们可以通过定位 SDK 的初始化、广告请求、广告展示等代码,来分析其逻辑,从而找到突破点。





分析开屏广告


3.1 分析步骤


3.1.1 分析广告页面


 

首先对开屏广告页面进行分析,通过MT管理器发现该广告是处在 WelcomeActivity 类中,我们直接hook 类,得到其函数调用栈。

 


3.1.2 分析启动时函数调用栈


可以猜测 showHomePage() 就是展示我们的主页了,我们逐条分析广告发生前的函数:


private void checkPermission() {
if (lpt2.br(InitHelper.getInstance().checkInitPermission(this))) {
jumpToMain();
return;
}
List<String> checkInitPermission = InitHelper.getInstance().checkInitPermission(this);
androidx.core.app.aux.a(this, (String[]) checkInitPermission.toArray(new String[checkInitPermission.size()]), 1);
}

// 检查初始化权限
public List<String> checkInitPermission(Context context) {
ArrayList<String> arrayList = new ArrayList();
ArrayList arrayList2 = new ArrayList();
arrayList.add("android.permission.INTERNET"); // 访问网络的权限
if (!org.qiyi.speaker.u.con.bMX()) {
arrayList.add("android.permission.READ_PHONE_STATE"); // 取手机状态的权限
}
arrayList.add("android.permission.WRITE_EXTERNAL_STORAGE"); // 写入外部存储设备的权限
arrayList.add("android.permission.ACCESS_NETWORK_STATE"); // 访问网络状态的权限
....
}

private void jumpToMain() {
Log.e("gzy", "size:" + SpeakerApplication.getInstance().getCurrentActivitySize());
// 用户是否给软件授权
if (!org.qiyi.speaker.o.con.bLa()) {
org.qiyi.speaker.o.con.a(this, this.mLisenceCallback); // 显示免责声明并进行用户许可
// 加载splash启动页动画(没有后台进程)
} else if (GuideController.INSTANCE.needShowSplashGuide()) {
showGuidePage();
} else {
//
launchMain(false);
}
}

// 首次打开,启动应用程序主界面
public void launchMain(final boolean z) {
// 如果当前Activity数量不等于1,那么显示主页。
if (SpeakerApplication.getInstance().getCurrentActivitySize() != 1) {
showHomePage(z);
return;
}
// 注册一个启动画面的回调,请求广告并下载,当启动画面结束后, 显示广告。
com.qiyi.video.g.con.aXh().registerSplashCallback(new ISplashCallback() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2
@Override // org.qiyi.video.module.api.ISplashCallback
public void onAdAnimationStarted() {
}

@Override // org.qiyi.video.module.api.ISplashCallback
public void onAdCountdown(int i) {
}

@Override // org.qiyi.video.module.api.ISplashCallback
public void onAdOpenDetailVideo() {
}

@Override // org.qiyi.video.module.api.ISplashCallback
public void onAdStarted(String str) {
}

@Override // org.qiyi.video.module.api.ISplashCallback
public void onSplashFinished(int i) {
WelcomeActivity.this.showHomePage(z);
JobManagerUtils.a(new Runnable() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2.1
@Override // java.lang.Runnable
public void run() {
com.qiyi.video.qysplashscreen.ad.aux.aUv().aUE();
((ISplashScreenApi) ModuleManager.getModule(IModuleConstants.MODULE_NAME_SPLASH_SCREEN, ISplashScreenApi.class)).requestAdAndDownload();
}
}, 500, PageAutoScrollUtils.HANDLER_SWITCH_NEXT_TIPS_DELAY, "splashAD_requestad", WelcomeActivity.TAG);
}
});
launchAppGuide();
}


3.1.3 修改 if 判断


可以看到当当前Activity数量不等于1时,就直接调 showHomePage 函数,我们可以将这个判断改为永真,让其直接显示主页。

 

 

重打包编译签名,运行程序,已去除开屏广告。


3.2 总结


对于开屏广告,我们可以观察应用启动的 Acitivity 顺序 (先从主入口切入Main),寻找其函数调用顺序,找到其播送广告的页面,将其逻辑更改,就可以屏蔽掉开屏广告。





分析播放视频广告


4.1 分析步骤


4.1.1 分析广告页面

 

首先对视频广告页面进行分析,有暂停键、静音键、详情键、持续时间、会员关闭提示…,我们可以想到:

◆剩余时间:获取广告时长,并设置计时器(可能会有判断时间归零,结束视频)
◆了解详情:获取广告 ID,设置按钮监听,保存广告详情 url
◆暂停键:保留当前广告播放位置

……


4.1.2 分析持续时间


本人选择剩余时间作为破解入口,通过开发者助手查到显示时间的资源 ID 是R.id.account_ads_time_pre_ad,搜索资源ID可得三处引用该资源。

 


 

通过 hook 分析发现在视频启动时的广告,调用的是 aux 类的函数:

 

 

分析 aux 类里使用了R.id.account_ads_time_pre_ad的方法,找到三处,分别分析:

 

第一、二处均用在Xi()函数中,该函数主要设置广告配置及布置广告界面。


private void Vz() {
......
this.bPB = (TextView) findViewById(R.id.account_ads_time_pre_ad);
}

private void Xi() {
...
// 获取当前广告播放器的状态
BaseState currentState = this.mAdInvoker.getCurrentState();
// 获取了广告播放器的UI策略
int adUIStrategy = this.mAdInvoker.getAdUIStrategy();
// 打印日志
com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_ROLL", "{GPhoneRollAdView}", " show ad UI, current state = ", currentState, ", adUiStrategy: ", Integer.valueOf(adUIStrategy));
// 设置视图的背景,根据当前广告播放器的状态来选择不同的背景资源
this.bPy.setBackgroundResource(currentState.isOnPaused() ? R.drawable.qiyi_sdk_play_ads_player : R.drawable.qiyi_sdk_play_ads_pause);
// 获取了当前广告的交付类型
int i = this.mDeliverType;
boolean z = i == 3 || i == 7 || i == 4;
// 获取广告播放器配置
QYPlayerADConfig adConfig = this.mAdInvoker.getAdConfig();
int i2 = 8;
// 根据UI策略的不同值,来设置一些视图的可见性或执行一些方法,8不可见,0可见
if (adUIStrategy == 1) {
this.bPA.setVisibility(8);
this.bPy.setVisibility(8);
this.bPF.setVisibility(8);
this.bPz.setVisibility(8);
} else if (adUIStrategy == 2) {
this.bPA.setVisibility(8);
this.bPy.setVisibility(8);
this.bPz.setVisibility(8);
this.bSv.setVisibility(8);
this.bSq.setVisibility(8);
this.bSq.setOnTouchListener(null);
} else if (adUIStrategy == 3) {
this.bPA.setVisibility(8);
this.bPF.setVisibility(8);
boolean isMute = isMute(); // 检查广告是否处于静音状态
this.bPL = isMute;
setAdMute(isMute, false);
} else {
this.bPF.setVisibility(0);
TextView textView = this.bPA;
if (!this.mIsLand) {
i2 = 0;
}
textView.setVisibility(i2);
boolean isMute2 = isMute();
this.bPL = isMute2;
setAdMute(isMute2, false);
Xk();
}
if (this.mDeliverType != 6) {
this.bPB.setVisibility(0); // 设置时间视图可显
}
this.bPB.setText(String.valueOf(this.mAdInvoker.getAdDuration())); // 给时间视图赋值
}


第三处位于Xc()函数中,根据 hook 到的函数调用栈,分析其运行过程:

 


public void Xc() {
// 获取广告播放时长
int adDuration = this.mAdInvoker.getAdDuration();
String str = adDuration + "";
...

jv(adDuration); // 判断能不能跳过广告
if (XE()) {
XH();
}
TextView textView = this.bPB; // 设置剩余时间
if (textView != null) {
textView.setText(str); // 显示非VIP持续时间
}
int i = this.mDeliverType;
if (i == 3 || i == 7) { // 如果交付类型是3或7 (VIP广告),广告持续时间小于1,调用dz(false)
if (adDuration < 1) {
dz(false);
} else {
this.bSA.setText(str); // 显示VIP持续时间
}
}
if (this.mDeliverType == 2) { // 允许跳过的广告
int Xp = Xp(); // 广告可跳过的剩余时间
if (Xp < 1) { // 允许跳过
Xl(); // 显示跳过按钮
} else {
this.bSG.setText(this.mContext.getString(R.string.trueview_accountime, Integer.valueOf(Xp)));
}
}
// 省流:根据不同的交付类型,为不同类型的广告进行时间配置与视图是否可显操作
...
}

// 处理广告的交互时间限制逻辑
private void jv(int i) {
// 判断是否为触摸广告,是否支持点击跳转,并且是否已经被点击过
if (!this.bOR.isTouchAd() || this.bOR.getClickThroughType() != 0 || this.bTn) {
return; // 是,直接返回
}
// 获取广告的预览信息
PreAD creativeObject = this.bOR.getCreativeObject();
// getInterTouchTime()是广告中点击交互的时间间隔,返回 10,表示用户需要等待至少 10 秒之后才能进行一次点击交互。小于0,说明可以点击。
// 后面一个条件是指当前时间加上最早允许交互的时间点,如果超过广告总时长,则不允许交互,比如总时长120秒,getInterTouchTime() 返回 40,当前时间为100秒,大于总时长,不允许交互。
if (creativeObject.getInterTouchTime() <= -1 || i + creativeObject.getInterTouchTime() > this.bTp) {
return;
}
// 重置广告界面,继续播放
this.bSq.reset();
Wu();
}

// 判断当前广告是创意广告
private boolean XE() {
CupidAD<PreAD> cupidAD = this.bOR;
if (cupidAD == null || cupidAD.getCreativeObject() == null) {
return false;
}
return this.bOR.getDeliverType() == 10 || this.bOR.getDeliverType() == 11;
}

// 计算广告可跳过的剩余时间
private int Xp() {
if (this.bOR.getDeliverType() != 2) {
return 0;
}
return (this.bOR.getSkippableTime() / 1000) - ((this.bOR.getDuration() / 1000) - this.mAdInvoker.getAdDuration());
}


上面两个函数都是对布局文件进行操作,设置其 text 或者是否可显,并没有判断去掉广告的地方,我们还有继续寻找。

 

对比两个函数发现,获取持续时间的函数是 getAdDuration(),我们去寻找该函数声明,发现在com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy类中:


public int getAdDuration() {
com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;
if (com1Var == null) {
return 0;
}
return com1Var.getAdsTimeLength();
}

// 位于 com.iqiyi.video.qyplayersdk.core.QYBigCorePlayer 类中
public int getAdsTimeLength() {
com8 com8Var = this.pumaPlayer;
if (com8Var != null) {
return Math.round(com8Var.GetADCountDown() / 1000.0f); // 转成整数
}
return 0;
}

// com.mcto.player.nativemediaplayer.NativeMediaPlayer 类中
public int GetADCountDown() {
int GetADCountDown;
if (IsCalledInPlayerThread()) { // 判断是否在播放器线程中调用
return this.native_media_player_bridge.GetADCountDown(); // 获取广告持续时间
}
synchronized (this) {
if (!this.native_player_valid) { // 判断播放器是否合法
throw new MctoPlayerInvalidException(puma_state_error_msg);
}
GetADCountDown = this.native_media_player_bridge.GetADCountDown();
}
return GetADCountDown;
}

// com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 类中
public int GetADCountDown() {
// 调用了一个指定ID为43的方法,该方法返回一个JSON格式的字符串,其中包含有关广告信息的数据
String InvokeMethod = InvokeMethod(43, "{}");
if (InvokeMethod.isEmpty()) { // 返回的字符串为空,则表示当前没有广告,方法返回0。
return 0;
}
try {
// 返回的字符串不为空,则将其转换为JSONObject对象,并获取其中名为ad_count_down的值
return new JSONObject(InvokeMethod).getInt("ad_count_down");
} catch (JSONException unused) {
return 0;
}
}


跟进到 com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 我们就可以发现,该软件是在Native层利用 mediaplay 获取视频时间信息。到这里获取剩余时间的 Java 层分析就差不多可以了。我们可以看到的是在NativeMediaPlayerBridge这个类中调用了众多 native 方法去获取广告的各种信息供后续操作,但是将所有的方法全修改一遍不太现实,我们需要寻找判断是否显示广告界面的地方。


4.1.3 分析 QYMediaPlayerProxy 代理类


根据 hook 上层类的方法调用发现,QYMediaPlayerProxy类中存在一些可能是与加载广告界面相关的函数。

 

 

几个重要的函数分析:


// setVVCollector():设置VVCollector,收集播放器的VV统计信息。
// video view (VV),意思为视频播放次数,根据广告播放次数,统计盈利。
public void setVVCollector(com.iqiyi.video.qyplayersdk.module.a.f.con conVar) {
com.iqiyi.video.qyplayersdk.module.a.aux auxVar = this.mStatistics;
if (auxVar != null) {
auxVar.setVVCollector(conVar);
}
}

// init(): 初始化播放器界面
// 获取了mControlConfig中的一些配置信息,例如编解码类型、是否自动跳过片头片尾、色盲模式等,然后调用prn.aux构造方法创建一个prn对象,并设置这些配置信息,最后通过a()方法将prn对象和mPassportAdapter对象一起传入a方法中,完成播放器的初始化。
public void init() {
this.mPlayerCore.a(new prn.aux(this.mControlConfig.getCodecType())
.eH(this.mControlConfig.isAutoSkipTitle())
.eI(this.mControlConfig.isAutoSkipTrailer())
.kR(this.mControlConfig.getColorBlindnessType())
.lX(this.mControlConfig.getExtendInfo())
.lY(this.mControlConfig.getExtraDecoderInfo())
.aie(), com.iqiyi.video.qyplayersdk.core.data.aux.a(this.mPassportAdapter));
}

// 检查 RC 策略是否需要执行
// RC 策略是指在不同的地理位置或网络环境下,根据不同的版权限制或合作协议,播放不同的内容或提供不同的服务。
public PlayData checkRcIfRcStrategyNeeded(PlayData playData) {
if (playData == null) {
com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded source == null!");
return playData;
}
int rCCheckPolicy = playData.getRCCheckPolicy();
com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded strategy == " + rCCheckPolicy);
if (this.mPlayerRecordAdapter == null) {
this.mPlayerRecordAdapter = new PlayerRecordAdapter();
}
// 根据 RCCheckPolicy (即 RC 策略) 的值。
// 如果值为 2,直接返回 playData;如果值为 1 或 0,,则调用 PlayerRecordAdapter 的 retrievePlayerRecord 方法,获取播放记录,
return rCCheckPolicy == 2 ? playData : (rCCheckPolicy == 1 || rCCheckPolicy == 0) ?
com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, this.mPlayerRecordAdapter.retrievePlayerRecord(playData)) : playData;
}

// 获取登录用户信息
void login() {
IPassportAdapter iPassportAdapter;
// mPlayerCore 是播放器核心,mPassportAdapter 是用户身份验证适配器。
if (this.mPlayerCore == null || (iPassportAdapter = this.mPassportAdapter) == null) {
return;
}
// 判断是不是VIP用户,并获取相应用户信息
this.mPlayerCore.login(com.iqiyi.video.qyplayersdk.core.data.aux.a(iPassportAdapter));
}

// 准备播放器重要核心配置
private void prepareBigCorePlayback(PlayData playData) {
boolean z;
org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.prepareBigCorePlayback");

// 检查是否需要预加载
com.iqiyi.video.qyplayersdk.h.con conVar = this.mPreload;
if (conVar != null) {
conVar.aoj();
}

// 根据播放数据和控制配置,选择一个播放策略,根据策略选择对应操作
int a2 = com.iqiyi.video.qyplayersdk.player.data.b.nul.a(playData, this.mContext, this.mControlConfig);
com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "vplay strategy : " + a2);
switch (a2) {
case 1:
performBigCorePlayback(playData);
break;
case 2:
z = true;
doVPlayBeforePlay(playData, z);
break;
case 3:
doVPlayFullBeforePlay(playData);
break;
case 4:
doVPlayAfterPlay(playData);
break;
case 5:
if (com.iqiyi.video.qyplayersdk.g.aux.isDebug()) {
throw new RuntimeException("address & tvid & ctype are null");
}
com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "address & tvid & ctype are null");
break;
case 6:
z = false;
doVPlayBeforePlay(playData, z);
break;
}
org.qiyi.android.coreplayer.d.com7.endSection();
}

// 视频播放结束后,继续获取视频的相关信息。
public void doVPlayAfterPlay(final PlayData playData) {
performBigCorePlayback(playData);
lpt6 lpt6Var = this.mTaskExecutor;
if (lpt6Var != null) {
lpt6Var.q(new Runnable() { // from class: com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy.1
@Override // java.lang.Runnable
public void run() {
QYMediaPlayerProxy.this.requestVplayInfo(playData);
}
});
}
}

// 在获取视频源前获取一些与视频相关的信息
private void doVPlayBeforePlay(PlayData playData, boolean z) {
VPlayParam a2 = com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, VPlayHelper.CONTENT_TYPE_PLAY_CONDITION, this.mPassportAdapter);
this.mVPlayHelper.cancel();
// 请求 VPlay 信息
this.mVPlayHelper.requestVPlay(this.mContext, a2, new aux(this, playData, this.mSigt, z), this.mBigcoreVplayInterceptor);
sendVPlayRequestPingback(true, playData, this.mSigt);
com.iqiyi.video.qyplayersdk.b.com3.b(playData);
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " doVPlayBeforePlay needRequestFull=", Boolean.valueOf(z));
}

// 判断是否需要网络拦截
private boolean isNeedNetworkInterceptor(PlayerInfo playerInfo) {
// 是否需要忽略用户代理的拦截
if (ignoreNetworkInterceptByUA()) {
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "ignoreNetworkInterceptByUA ");
return false;
}

// 判断当前是否处于离线状态,并且要播放的视频是在线视频
boolean gW = org.iqiyi.video.l.aux.gW(this.mContext);
boolean D = com.iqiyi.video.qyplayersdk.player.data.b.nul.D(playerInfo);
if (gW && D) {
// 获取当前的错误码版本号,根据不同的版本号来执行不同的逻辑
int errorCodeVersion = getErrorCodeVersion();
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "isNeedNetworkInterceptor isOffNetWork = ", Boolean.valueOf(gW), " isOnLineVideo = ", Boolean.valueOf(D), " errorCodeVer = " + errorCodeVersion);

if (errorCodeVersion == 1) {
// 自定义错误码为900400的播放器错误
this.mInvokerQYMediaPlayer.onError(PlayerError.createCustomError(900400, "current network is offline, but you want to play online video"));
return true; // 进行网络拦截

} else if (errorCodeVersion == 2) {
// 返回错误码和错误信息
org.iqiyi.video.data.com7 bbQ = org.iqiyi.video.data.com7.bbQ();
bbQ.xC(String.valueOf(900400));
bbQ.setDesc("current network is offline, but you want to play online video");
this.mInvokerQYMediaPlayer.onErrorV2(bbQ);
return true;
}
}
return false; // 不需要进行网络拦截
}


我们重点分析performBigCorePlayback函数:


// 执行播放器的核心播放功能
private void performBigCorePlayback(PlayData playData, PlayerInfo playerInfo, String str) {
int i;

// 判断是否有自定义的播放拦截器(mDoPlayInterceptor),如果有且拦截器拦截了播放请求,则不播放视频。
com.iqiyi.video.qyplayersdk.f.con conVar = this.mDoPlayInterceptor;
if (conVar != null && conVar.e(playerInfo)) {
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "DoPlayInterceptor is intercept!");
lpt5 lpt5Var = this.mInvokerQYMediaPlayer;
if (lpt5Var == null) {
return;
}
lpt5Var.amX();

// 没有播放器信息,什么都不做
} else if (this.mPlayerInfo == null) {
}

// 重点
else {
org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.performBigCorePlayback");
// 通过判断播放数据(playData)是否为空以及是否存在播放地址,空则i = 0。
if (com.iqiyi.video.qyplayersdk.player.data.b.nul.A(playerInfo) || playData == null) {
i = 0;
} else {
// 如果有地址,根据该数据生成CupidVvId,并将该ID与广告相关的Ad对象(mAd)绑定。
// 所以这里就是去后台获取广告的id
com.iqiyi.video.qyplayersdk.cupid.data.model.com9 a2 = com.iqiyi.video.qyplayersdk.cupid.util.con.a(playData, playerInfo, false, this.mPlayerRecordAdapter, 0);
a2.eV(isIgnoreFetchLastTimeSave());
int generateCupidVvId = CupidAdUtils.generateCupidVvId(a2, playData.getPlayScene());
com.iqiyi.video.qyplayersdk.cupid.com4 com4Var = this.mAd;

if (com4Var != null) {
com4Var.la(generateCupidVvId); // 更新当前的广告ID
}
org.qiyi.android.coreplayer.d.aux.boe();
i = generateCupidVvId;
}

// a3 存储广告信息
com.iqiyi.video.qyplayersdk.core.data.model.com1 a3 = com.iqiyi.video.qyplayersdk.core.data.a.aux.a(this.mSigt, i, playData, playerInfo, str, this.mControlConfig);
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " performBigCorePlayback QYPlayerMovie=", a3);
this.mPlayerInfo = new PlayerInfo.Builder().copyFrom(playerInfo).extraInfo(new PlayerExtraInfo.Builder().copyFrom(playerInfo.getExtraInfo()).sigt(a3.getSigt()).build()).build();
// 通知播放器信息已更改(在这里是指开始播放广告)
notifyPlayerInfoChanged();
// 判断是否断网
if (!isNeedNetworkInterceptor(playerInfo)) {
if (playData == null || (TextUtils.isEmpty(playData.getPlayAddress()) && (TextUtils.isEmpty(playData.getTvId()) || "0".equals(playData.getTvId())))) {
PlayerExceptionTools.report(0, 0.1f, "1", com.iqiyi.video.qyplayersdk.player.data.b.con.i(playData));
}
com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;
if (com1Var != null) {
com1Var.setVideoPath(a3); // 设置广告url
this.mPlayerCore.ahF();
}
}
org.qiyi.android.coreplayer.d.com7.endSection();
}
}

// 停止视频
public void amX() {
d dVar = this.mQYMediaPlayer;
if (dVar != null) {
dVar.stopPlayback();
}
}

// 判断是否获取到视频
public static boolean A(PlayerInfo playerInfo) {
return z(playerInfo) || y(playerInfo);
}

// 获取PlayerExtraInfo对象的播放地址和播放地址类型
public static boolean z(PlayerInfo playerInfo) {
if (playerInfo == null || playerInfo.getExtraInfo() == null) {
return false;
}
PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();
String playAddress = extraInfo.getPlayAddress();
int playAddressType = extraInfo.getPlayAddressType();
if (TextUtils.isEmpty(playAddress)) {
return false;
}
return playAddressType == 9 || playAddressType == 4 || playAddressType == 8;
}

// 判断是否有视频和专辑ID
public static boolean y(PlayerInfo playerInfo) {
String s = s(playerInfo); // 专辑ID
String u = u(playerInfo); // 视频ID
if ((TextUtils.isEmpty(s) || TextUtils.equals(s, "0")) && !((!TextUtils.isEmpty(u) && !TextUtils.equals(u, "0")) || playerInfo == null || playerInfo.getExtraInfo() == null)) {
// 获取PlayerExtraInfo对象的播放地址和播放地址类型
PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();
return !TextUtils.isEmpty(extraInfo.getPlayAddress()) && extraInfo.getPlayAddressType() == 6;
}
return false;
}

// 获取专辑ID
public static String s(PlayerInfo playerInfo) {
String id;
return (playerInfo == null || playerInfo.getAlbumInfo() == null || (id = playerInfo.getAlbumInfo().getId()) == null) ? "" : id;
}

// 获取视频ID
public static String u(PlayerInfo playerInfo) {
String id;
return (playerInfo == null || playerInfo.getVideoInfo() == null || (id = playerInfo.getVideoInfo().getId()) == null) ? "" : id;
}

// 一个广告控制器方法,用于更新当前的CupidvvId
public void la(int i) {
// col=0,则说明当前没有活跃的vvId,打印日志信息表示要更新当前的vvId
if (this.col.getAndIncrement() == 0) {
com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. current doesn't has active vvId.");
} else {
com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. but current has active vvId.");
// 将旧的vvId赋值给coh变量
this.coh = this.coi;
}
// 将当前新的ID赋给coi
this.coi = i;
lc(i);
com5.aux auxVar = this.mQYAdPresenter;
if (auxVar != null) {
auxVar.lh(i); // 为暂停播放函数与继续播放函数传递广告ID
}
}

/*
该方法用于注册广告委托和委托JSON,以展示广告
通过 qYPlayerADConfig3.checkRegister 方法判断是否需要注册广告
通过 Cupid.registerObjectAppDelegate 方法注册代理
广告类型包括:
中插广告(SlotType.SLOT_TYPE_BRIEF_ROLL)、
viewpoint广告(SlotType.SLOT_TYPE_VIEWPOINT)、
页面广告(SlotType.SLOT_TYPE_PAGE)等等
代码过长就不再此展示,需要请自行查看
*/
private void lc(final int i) {
com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK_AD_CORE", "{AdsController}", "; registerCupidJsonDelegate vvId:", Integer.valueOf(i), "");

org.qiyi.android.coreplayer.d.aux.wr(com.qiyi.baselib.utils.d.nul.fJ(org.iqiyi.video.mode.com3.enn) ? 2 : 1);
...
QYPlayerADConfig qYPlayerADConfig5 = this.cog;
if (qYPlayerADConfig5.checkRegister(256, qYPlayerADConfig5.getAddAdPolicy())) {
QYPlayerADConfig qYPlayerADConfig6 = this.cog;
if (!qYPlayerADConfig6.checkRegister(256, qYPlayerADConfig6.getRemoveAdPolicy())) {
Cupid.registerJsonDelegate(i, SlotType.SLOT_TYPE_VIEWPOINT.value(), this.cof);
}
}
...
}


我们可以发现这个函数就是判断是否显示广告界面的函数,可以猜测只有当是VIP账户时,播放数据(playData)才为空,才会使 i = 0(广告ID为0)。


4.1.4 修改 if 判断


到这里我们就可以尝试进行破解了,将 if 判断修改,使之进入 i=0 的分支中。

 

 

重打包编译签名,运行程序,已去除视频广告。


4.2 总结


4.2.1 破解广告技巧


◆对于破解视频广告或其他广告,都可以通过获取广告的相关控件,分析函数的调用逻辑顺序,定位到关键类,分析类,找到关键函数。


◆针对复杂客户端,尽量不采用关键字搜索的方式去破解,因为复杂的客户端代码都是有设计思想的,并且大概率做了混淆,无法轻易通过关键字符串进行定位,可以尝试通过资源 ID 进行定位。


◆提高英文水平,如 player 代表播放器、Ad Duration 代表广告持续时间等,破解的首要任务就是看懂代码,特别是对于混淆过的代码,那些没有混淆过的函数名、变量名就是破解的关键,只有看懂才有机会能猜到关键点。


4.2.2 扩展:proxy代理类


分析代码后发现,广告的生成、调用、配置大部分都是在QYMediaPlayerProxy类中完成的,并且播放器的核心功能也有一部分在代理类中调用。

 

对于第三方SDK动态导入视频广告,通常会通过网络请求向广告服务器发送请求以获取广告,流程参考下方 android 广告 SDK 原理流程图,常用方法使通过动态代理,通过动态代理这样的方法有一定的好处:


1.可以过滤和控制广告流量,例如阻止一些恶意或不受欢迎的广告,以及提高广告访问速度和可靠性。


2.在特殊情况下,广告服务器可能会要求使用特定的代理服务器或 IP 地址进行广告请求。这时候,动态代理就可以被用来实现这些特殊的网络访问要求,确保广告请求能够成功发送和接收。


进一步分析,我们可以想到广告不太会是在软件刚出来时就加上,一定是后续附加上去的功能。后续除了广告之外肯定也会陆续附加其他功能,如何做到这些功能扩展呢?这就可以用 proxy 代理类了,将播放器核心功能(播放视频)融入到代理类中,让其负责对核心功能进行扩展(如在播放视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。

 

android 广告 SDK 原理流程图

 

 

参考链接:

 

Android反编译实战-去广告_安卓反编译去除广告_sam.li的博客

https://blog.csdn.net/samlirongsheng/article/details/111684432

 

android广告SDK原理详解(附源码) - 爱码网 (likecs.com)

https://www.likecs.com/show-204598624.html





看雪ID:I鲸落I

https://bbs.kanxue.com/user-home-939796.htm

*本文由看雪论坛 I鲸落I 原创,转载请注明来自看雪社区


往期推荐

特斯拉最新电驱控制器变化拆解梳理及其三电系统供应链

仅剩1位73岁开发者苦撑!能求解超复杂物理方程式的计算程序,要没人维护了

【交易技术前沿】国产分布式数据库在证券行业的应用价值

7 种提升 Spring Boot 吞吐量神技



球在看

继续滑动看下一个
大白聊IT
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存